<?php
namespace app\command\extend\http;



use app\command\HttpServer;
use app\command\Swoole;
use app\common\model\coin\CoinRuleRun;
use think\Exception;

abstract class Base
{

    /**
     * 存放 ENV 参数的组
     */
    const ENV_GROUP = 'http_extend';

    /**
     * 记录日志的处理
     */
    const LOG = [
        'RECORD'    => [
            'type'  => 'RECORD',
            'CUSTOM_TYPE'   => 'EXTEND',
        ],
        'ERROR' => [
            'type'  => 'ERROR',
            'CUSTOM_TYPE'   => 'EXTEND',
        ],
    ];

    /**
     * 类名
     * @var null
     */
    static $classname = null;

    /**
     * 创建表数
     * @var array
     */
    static $tableList = [];

    /**
     * 获取方法限制信息
     * @var array ['方法名'=>['ip'=>[限制的IP数组],'params'=>[限制的参数字段]]]    // ip 数组空表示未限制
     */
    protected $methodLimitList = [];

    /**
     * 设置方法请求的前缀
     * @var string
     */
    protected $methodCallPrefix = 'call';

    /**
     * 错误信息
     * @var string[]
     */
    protected $errorType = [
        'UNKNOWN'   => 'unknown error', // 未知错误
        'LIMIT_IP'  => 'IP [%s] has no permission',  // IP 限制
        'METHOD'    => 'Method [%s] does not exist',  //方法不存在
        'PARAMS'    => 'Missing parameter [%s]',  //参数错误
    ];

    // 获取类名 [返回类名字符串]
    abstract protected function getClassname();

    /**
     * 获取限制
     * @return mixed 格式：['method'=>['ip'=>’限制的IP‘，’params‘=>’限制的参数‘]] 【default 表示默认】
     */
    abstract protected function getLimit();

    /**
     * 获取需要初始化的表
     * @return mixed  输出数组： ['表别名'=>['表名',是否初始化,是否使用锁,记录数（ 自动乘以 1024）,字段参数（数组）[[字段,类型,长度],[字段,类型,长度]]]]
     */
    protected function getTableList() {
        return [];
    }

    // 获取额外的错误类型列表 格式：['类型'=>'格式字符串']
    protected function getErrorTypeList() {
        return [];
    }

    // 初始化信息 【自定义其他操作】
    protected function init() {
        return true;
    }


    /**
     * 初始化处理
     * Base constructor.
     */
    public function __construct($onlyLog = false)
    {
        // 获取类名
        self::$classname = $this->getClassname();

        /******* 获取额外的错误类型 **********/
        if($errorTypeList = $this->getErrorTypeList()) {
            foreach ($errorTypeList as $error=>$value) {
                $this->errorType[$error] = $value;
            }
        }
        /***** END 获取额外的错误类型 ********/

        if($onlyLog) {    // 判断仅为日志时处理
            return true;
        }

        /******* 获取初始化表 ******/
        self::$tableList = $this->getTableList();
        if(empty(self::$tableList)) {
            self::$tableList = [];
        } else {
            self::swooleTable();
        }
        /***** END 获取初始化表 ****/


        /*********** 获取 IP 及参数的限制 ***********/
        $fields = ['ip','params'];
        if(!$limit = $this->getLimit()) {
            $limit = [];
        }
        if(!array_key_exists('default',$limit) || !is_array($limit['default'])) {
            $limit['default'] = [
                'ip'        => [],
                'params'    => [],
            ];
            $limit['default'] = [];
        }
        foreach($limit as $field=>$value) {
            $value = array_merge($limit['default'],is_array($value)?$value:[]);
            $limitDetails = [];
            foreach($fields as $fieldOne) {
                $limitDetails[$fieldOne] = array_key_exists($fieldOne,$value) && is_array($value[$fieldOne])?$value[$fieldOne]:[];
            }
            $this->methodLimitList[$field] = $limitDetails;
        }
        /********* END 获取 IP 及参数的限制 *********/

        // 其他自动初始化
        return $this->init();
    }

    /**
     * 请求调用
     * @param string $method 请求的方法
     * @param array $params 返回的参数
     * @param string $requestIp 客户端请求的IP
     * @return string
     */
    public function call(string $method,array $params,string $requestIp) {
        try {
            $call = $this->methodCallPrefix . $method;
            if(!method_exists($this,$call)) {   // 判断方法是否存在
                $this->error('METHOD',[$method]);
            }
            // 获取限制信息
            $limit = array_merge($this->methodLimitList['default'],array_key_exists($method,$this->methodLimitList)?$this->methodLimitList[$method]:[]);

            // 限制IP
            if(!empty($limit['ip']) && !in_array($requestIp,$limit['ip'])) {
                $this->error('LIMIT_IP',[$requestIp]);
            }

            // 限制参数
            foreach($limit['params'] as $field) {
                if(!array_key_exists($field,$params)) {
                    $this->error('PARAMS',[$field]);
                }
            }
            // 请求调用对应的方法
            $result = call_user_func_array([$this,$call],[$params]);
        }catch (\Exception $e) {
            if($e->getCode() == -1) {
                $e = [
                    'message'   => $e->getMessage(),
                    'method'    => $method,
                    'params'    => $params,
                ];
            }
            $this->log($e,'ERROR');
            throw new \Exception('api extend method error');
        }

        return $result;
    }


    /**
     * 添加跳转到 task 任务处理
     * @param array $result 返回的结果集
     * @param $method   回调方法
     * @param $params   参数
     */
    protected function task(array &$result,$method,$params) {
        $params = [
            'method'    => $method,
            'params'    => $params,
        ];
        $result['Task'] = [get_called_class(),'execTask',$params];
    }

    /**
     * 执行 task 处理【自动调用task方法】
     * @param $params
     */
    static function execTask($params) {
        $method = 'task';
        try {
            $method = 'task' . strtoupper($params['method'][0]) . substr($params['method'],1);
            $params = $params['params'];
            call_user_func_array([get_called_class(),$method],[$params]);
        } catch (\Exception $e) {
            if($e->getCode() == -1) {
                $e = [
                    'message'   => $e->getMessage(),
                    'method'    => $method,
                    'params'    => $params,
                ];
            }
            self::logStatic($e,'ERROR');
        }
        return true;
    }


    /**
     * 获取 env 值
     * @param string $name 模块下的名
     * @param string $default 默认值，注意，返回空也等于默认值
     * @param string $split 是否对结果按指定字符进行分割
     * @return false|string|string[] 返回字符串或分割后的数组
     */
    protected function env(string $name,$default = '',$split = '') {
        $value = trim(env(self::ENV_GROUP . '.' . $name));
        $value =  $value === ''?$default:$value;
        return empty($split)?$value:explode($split,$value);
    }

    /**
     * 去除小数点后面额外的 0
     * @param $price
     * @return string|string[]|null
     */
    static function trimZero($price) {
        return CoinRuleRun::trimZero(self::convert_scientific_number_to_normal($price));
    }


    /**
     * 抛出错误信息
     * @param string $type 错误类型
     * @param array|null $params 额外参数
     * @throws \Exception
     */
    public function error(string $type,array $params = null) {
        if(array_key_exists($type,$this->errorType)) {
            $message = $this->errorType[$type];
            if(!empty($params)) {
                array_unshift($params,$message);
                $message = call_user_func_array('sprintf',$params);
            }
        } else {
            $message = $this->errorType['UNKNOWN'];
        }
        throw  new \Exception($message,-1);
    }


    /**
     * 写日志
     */
    public function log($message,$type = 'RECORD') {
        $serializeMessage = false;
        if(!is_object($message)) {
            $message = [self::$classname,$message];
            $serializeMessage = true;
        }
        HttpServer::log($message,self::LOG[$type]['CUSTOM_TYPE'],self::LOG[$type]['type'],$serializeMessage);
    }


    /**
     * 静态日志处理【参数与 log 同步】
     */
    static protected function logStatic() {
        $classname = get_called_class();
        return call_user_func_array([(new $classname(true)),'log'],func_get_args());
    }

    /**
     * 静态处理中的错误返回
     * @return mixed
     */
    static protected function errorStatic() {
        $classname = get_called_class();
        return call_user_func_array([(new $classname(true)),'error'],func_get_args());
    }

    /**
     * 创建及获取表数据
     * @param null $tableKey
     * @param string|null $tableSuffix
     * @return bool|mixed
     */
    static protected function swooleTable($tableKey = null,string $tableSuffix = null) {
        $pre = strtolower(self::$classname) . '_';
        if(is_null($tableKey)) {    // 初始化表
            $tableList = self::$tableList;
            foreach($tableList as $table) {
                if(!$table[1]) {
                    continue;
                }
                // 创建Swoole表
                Swoole::table($pre . $table[0],$table[4],1024 * $table[3],$table[2]);
            }
            return true;
        }

        // 创建Swoole表
        $table = self::$tableList[$tableKey];
        if($tableSuffix) {
            $table[0] .= $tableSuffix;
        }
        return Swoole::table($pre . $table[0],$table[4],1024 * $table[3],$table[2]);
    }

    /**
     * 将科学计数法的数字转换为正常的数字
     * 为了将数字处理完美一些，使用部分正则是可以接受的
     * @author loveyu
     * @param string $number
     * @return string
     */
    static function convert_scientific_number_to_normal($number)
    {
        if(stripos($number, 'e') === false) {
            //判断是否为科学计数法
            return $number;
        }

        if(!preg_match(
            "/^([\\d.]+)[eE]([\\d\\-\\+]+)$/",
            str_replace(array(" ", ","), "", trim($number)), $matches)
        ) {
            //提取科学计数法中有效的数据，无法处理则直接返回
            return $number;
        }

        //对数字前后的0和点进行处理，防止数据干扰，实际上正确的科学计数法没有这个问题
        $data = preg_replace(array("/^[0]+/"), "", rtrim($matches[1], "0."));
        $length = (int)$matches[2];
        if($data[0] == ".") {
            //由于最前面的0可能被替换掉了，这里是小数要将0补齐
            $data = "0{$data}";
        }

        //这里有一种特殊可能，无需处理
        if($length == 0) {
            return $data;
        }

        //记住当前小数点的位置，用于判断左右移动
        $dot_position = strpos($data, ".");
        if($dot_position === false) {
            $dot_position = strlen($data);
        }

        //正式数据处理中，是不需要点号的，最后输出时会添加上去
        $data = str_replace(".", "", $data);


        if($length > 0) {
            //如果科学计数长度大于0

            //获取要添加0的个数，并在数据后面补充
            $repeat_length = $length - (strlen($data) - $dot_position);
            if($repeat_length > 0) {
                $data .= str_repeat('0', $repeat_length);
            }

            //小数点向后移n位
            $dot_position += $length;
            $data = ltrim(substr($data, 0, $dot_position), "0").".".substr($data, $dot_position);

        } elseif($length < 0) {
            //当前是一个负数

            //获取要重复的0的个数
            $repeat_length = abs($length) - $dot_position;
            if($repeat_length > 0) {
                //这里的值可能是小于0的数，由于小数点过长
                $data = str_repeat('0', $repeat_length).$data;
            }

            $dot_position += $length;//此处length为负数，直接操作
            if($dot_position < 1) {
                //补充数据处理，如果当前位置小于0则表示无需处理，直接补小数点即可
                $data = ".{$data}";
            } else {
                $data = substr($data, 0, $dot_position).".".substr($data, $dot_position);
            }
        }
        if($data[0] == ".") {
            //数据补0
            $data = "0{$data}";
        }
        return trim($data, ".");
    }


    /**
     * 请求数据
     * @param $method
     * @param array $params
     */
    static function request($method,array $params) {
        $classname = explode('\\',get_called_class());
        $result =  HttpServer::request(array_pop($classname),array_merge($params,[
            'method' => $method
        ]),'127.0.0.1:' . HttpServer::serverPort());
        try {
            $result = json_decode($result,true);
            if(is_array($result) && array_key_exists('status',$result) && $result['status'] == 'ok') {
                return $result['data'];
            }
        }catch (\Exception $e) {
        }
        return null;
    }


}